﻿//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------

using System;
using System.Collections.Generic;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.ResourceManagement;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.WSEnumerate;
using Microsoft.ResourceManagement.Samples.ResourceManagementClient.Utilities;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Xml;

namespace Microsoft.ResourceManagement.Samples.ResourceManagementClient
{
    /// <summary>
    /// This class abstracts the WS-Enumeration interface for the Resource Management Service.
    /// 
    /// Please note that this class is not thread-safe.
    /// </summary>
    public class WSEnumerationClient : IDisposable
    {
        private int pullAmount;
        private String enumerationContext;
        private SearchClient searchClient;
        private int numItems;
        private DateTime expires;

        public WSEnumerationClient()
            : this(Settings.Default.WsEnumerationDefaultPull, Constants.SearchClientDefaultConfig)
        {
        }

        public WSEnumerationClient(int defaultPullAmount)
            : this(defaultPullAmount, Constants.SearchClientDefaultConfig)
        {

        }

        public WSEnumerationClient(int defaultPullAmount, String clientConfigName)
        {
            if (defaultPullAmount <= 0)
                throw new ArgumentException("The pull amount must be a positive integer", "defaultPullAmount");

            this.searchClient = new SearchClient(clientConfigName);
            this.pullAmount = defaultPullAmount;
            // Setting to min value so the default state is expiration is in the past
            // Leaving as non-nullable to avoid complexities of null structs
            this.expires = DateTime.MinValue;
        }

        /// <summary>
        /// Starts an enumeration session using the provided filter string.
        /// By default we use the XPath filter dialect for the filter.
        /// 
        /// Please note that this method must be called before any other method as the state is stored internally.
        /// </summary>
        /// <param name="enumerateFilter">the xpath filter</param>
        public void Enumerate(String enumerateFilter)
        {
            this.Enumerate(enumerateFilter, null, null);
        }

        public void Enumerate(String enumerateFilter, List<string> selectionAttributes, List<string> sortingAttributes)
        {
            if (String.IsNullOrEmpty(enumerateFilter))
            {
                throw new ArgumentException("The xPath filter must not be empty or null");
            }
            EnumerateRequest enumerateRequest = new EnumerateRequest();
            enumerateRequest.Filter.Filter = enumerateFilter;

            if (selectionAttributes != null)
            {
                enumerateRequest.Selection = selectionAttributes.ToArray();
            }

            if (sortingAttributes != null)
            {
                int count = sortingAttributes.Count;
                enumerateRequest.Sorting = new Sorting();
                enumerateRequest.Sorting.SortingAttributes = new SortingAttribute[count];
                for (int i = 0; i < count; i++)
                {
                    enumerateRequest.Sorting.SortingAttributes[i] = new SortingAttribute(sortingAttributes[i], true);
                }
            }

            Message enumerateRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSEnumerationEnumerateAction, enumerateRequest, new ClientSerializer(typeof(EnumerateRequest)));

            #region Temporary
            Console.WriteLine(enumerateRequestMessage);
            Console.WriteLine();
            #endregion

            Message responseMessage = this.searchClient.Enumerate(enumerateRequestMessage);

            #region Temporary
            Console.WriteLine(responseMessage);
            Console.WriteLine();
            #endregion

            StaticSchemaManager.Instance.CheckFault(responseMessage);

            EnumerateResponse myResponse = responseMessage.GetBody<EnumerateResponse>(new ClientSerializer(typeof(EnumerateResponse)));
            this.enumerationContext = myResponse.EnumerationContext;
            if (null != myResponse.EnumerationDetail)
                this.numItems = myResponse.EnumerationDetail.Count;
            if (false == DateTime.TryParse(myResponse.Expires, out this.expires))
            {
                this.expires = DateTime.MaxValue;
            }
        }

        /// <summary>
        /// Pull the default number of items from the server from the previous filter.
        /// Must call enumerate before calling this method.
        /// </summary>
        /// <returns>A list of objects returned from the server.</returns>
        public IList<ResourceManagementObject> Pull()
        {
            return Pull(this.numItems);
        }

        /// <summary>
        /// Pull the specified number of items from the server from the previous filter.
        /// Must call enumerate before calling this method.
        /// </summary>
        /// <param name="maxItems"></param>
        /// <returns></returns>
        public IList<ResourceManagementObject> Pull(int maxItems)
        {
            return Pull(null, maxItems);
        }

        public IList<ResourceManagementObject> Pull(int? startingIndex, int maxItems)
        {
            this.CheckValidContext();
            if (maxItems <= 0)
                throw new ArgumentException("maxItems must be a positive integer", "maxItems");
            PullRequest pullRequest = new PullRequest();
            pullRequest.EnumerationContext = this.enumerationContext;
            pullRequest.MaxCharacters = Int32.MaxValue;
            pullRequest.MaxElements = maxItems;

            if (startingIndex != null)
            {
                pullRequest.PullAdjustment = new PullAdjustmentType();
                pullRequest.PullAdjustment.StartingIndex = startingIndex.ToString();
            }

            Message pullRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSEnumerationPullAction, pullRequest, new ClientSerializer(typeof(PullRequest)));

            #region Temporary
            Console.WriteLine(pullRequestMessage);
            Console.WriteLine();
            #endregion

            Message responseMessage = this.searchClient.Pull(pullRequestMessage);

            #region Temporary
            Console.WriteLine(responseMessage);
            Console.WriteLine();
            #endregion

            StaticSchemaManager.Instance.CheckFault(responseMessage);

            PullResponse myResponse = responseMessage.GetBody<PullResponse>(new ClientSerializer(typeof(PullResponse)));
            this.enumerationContext = myResponse.EnumerationContext;

            List<ResourceManagementObject> retList = new List<ResourceManagementObject>();

            foreach (XmlNode item in myResponse.Items)
            {
                foreach (XmlNode child in item.SelectNodes("/rm:*", StaticSchemaManager.Instance.ResourceManagementNamespaceManager))
                {
                    ResourceManagementObject resourceManagementObject = ResourceManagementObjectFactory.CreateObject(child);
                    if (null != resourceManagementObject)
                        retList.Add(resourceManagementObject);
                }
                if (item.Name.Equals("EndOfSequence") || item.SelectSingleNode("//wsen:EndOfSequence", StaticSchemaManager.Instance.ResourceManagementNamespaceManager) != null)
                {
                    this.enumerationContext = String.Empty;
                }
            }

            return retList;
        }

        /// <summary>
        /// Updates the expiration time.
        /// Must call enumerate before calling this method.
        /// </summary>
        public void GetStatus()
        {
            this.CheckValidContext();
            GetStatusRequest getStatusRequest = new GetStatusRequest();
            getStatusRequest.EnumerationContext = this.enumerationContext;

            Message getStatusRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSEnumerationGetStatusAction, getStatusRequest, new ClientSerializer(typeof(GetStatusRequest)));

            Message responseMessage = this.searchClient.GetStatus(getStatusRequestMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);

            GetStatusResponse myResponse = responseMessage.GetBody<GetStatusResponse>(new ClientSerializer(typeof(GetStatusResponse)));
            if (false == DateTime.TryParse(myResponse.Expires, out this.expires))
            {
                this.expires = DateTime.MaxValue;
            }
        }

        /// <summary>
        /// Instructs the server to discard the session.  Cannot call any method after this one.
        /// </summary>
        public void Release()
        {
            this.CheckValidContext();
            ReleaseRequest releaseRequest = new ReleaseRequest();
            releaseRequest.EnumerationContext = this.enumerationContext;

            Message releaseRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSEnumerationReleaseAction, releaseRequest, new ClientSerializer(typeof(ReleaseRequest)));
            
            Message responseMessage = this.searchClient.Release(releaseRequestMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);
        }

        /// <summary>
        /// Asks the server to extend the expiration time of the session by the default amount of time.
        /// </summary>
        public void Renew()
        {
            Renew(DateTime.Now.AddMinutes(Settings.Default.WsEnumerationDefaultExpiresAdd));
        }

        /// <summary>
        /// Asks the server to extend the expiration time of the session by the given amount of time.
        /// </summary>
        /// <param name="requestedExpiration">The new expiration time of the session.</param>
        public void Renew(DateTime requestedExpiration)
        {
            this.CheckValidContext();
            RenewRequest renewRequest = new RenewRequest();
            renewRequest.EnumerationContext = this.enumerationContext;
            renewRequest.Expires = requestedExpiration;

            Message renewRequestMessage = Message.CreateMessage(MessageVersion.Default, Constants.WSEnumerationRenewAction, renewRequest, new ClientSerializer(typeof(RenewRequest)));

            Message responseMessage = this.searchClient.Renew(renewRequestMessage);
            StaticSchemaManager.Instance.CheckFault(responseMessage);

            RenewResponse myResponse = responseMessage.GetBody<RenewResponse>(new ClientSerializer(typeof(RenewResponse)));
            this.enumerationContext = myResponse.EnumerationContext;
            DateTime prevExpires = this.expires;
            if (false == DateTime.TryParse(myResponse.Expires, out this.expires))
            {
                this.expires = prevExpires;
            }
        }

        private void CheckValidContext()
        {
            if (String.IsNullOrEmpty(this.enumerationContext))
                throw new InvalidOperationException("The enumeration context is invalid.  Call Enumerate again and be sure to Renew as appropriate.");
        }

        /// <summary>
        /// The number of items in the current session.
        /// </summary>
        public int Count
        {
            get
            {
                return numItems;
            }
        }

        /// <summary>
        /// The time of expiration of the current session.
        /// </summary>
        public DateTime Expires
        {
            get
            {
                return this.expires;
            }
        }
        /// <summary>
        /// Whether the current session is valid.
        /// </summary>
        public bool IsContextValid
        {
            get
            {
                return String.IsNullOrEmpty(this.enumerationContext) == false;
            }
        }
        /// <summary>
        /// Provides the network credentials the client uses to connect to the server.
        /// </summary>
        public System.Net.NetworkCredential ClientCredentials
        {
            get
            {
                return this.searchClient.ClientCredentials.Windows.ClientCredential;
            }
            set
            {
                this.searchClient.ClientCredentials.Windows.ClientCredential = value;
            }
        }

        public int NumItems
        {
            get
            {
                return numItems;
            }
        }


        #region IDisposable Members

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        public void Dispose()
        {
            if (null != this.searchClient)
            {
                this.searchClient.Close();
                this.searchClient = null;
            }
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool isManaged)
        {
            if (null != this.searchClient)
            {
                this.searchClient.Close();
                this.searchClient = null;
            }
        }

        #endregion
    }
}
